package de.jmda.core.util;

import static de.jmda.core.util.StringUtil.sb;
import static java.lang.System.lineSeparator;
import static java.util.Arrays.asList;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import javax.annotation.processing.Processor;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;

public abstract class JavaCompilerUtil
{
	private final static Logger LOGGER = Logger.getLogger(JavaCompilerUtil.class);

	/**
	 * Best effort approach to create a {@link JavaCompiler}.
	 * <p>
	 * If {@link ToolProvider#getSystemJavaCompiler()} returns a non <code>null
	 * </code> value that value will be returned.
	 * <p>
	 * Else reflection will be used to instantiate a new java compiler. If this
	 * succeeds the instance will be returned.
	 *
	 * @return see above
	 * @throws IllegalStateException if best effort approach failed
	 */
	public static JavaCompiler createJavaCompiler() throws IllegalStateException
	{
		JavaCompiler result = ToolProvider.getSystemJavaCompiler();

		if (result == null)
		{
			Class<?> javaCompilerClass;

			try
			{
				javaCompilerClass = Class.forName(JavaCompiler.class.getName());
			}
			catch (ClassNotFoundException e)
			{
				throw new IllegalStateException(
						  "java compiler not available, maybe tools.jar is not in the "
						 + "classpath (use a jdk instead of a jre)?", e);
			}

			try
			{
				result = (JavaCompiler) javaCompilerClass.newInstance();
			}
			catch (InstantiationException | IllegalAccessException e)
			{
				throw new IllegalStateException(
				      "failure instantiating java compiler, please make sure you are "
				    + "running java.exe from a jdk and not from a jre", e);
			}
		}

		return result;
	}

	/**
	 * Best effort approach to perform a standard compilation of compilation units
	 * together with a compiler processor.
	 *
	 * @param compiler
	 * @param compilationUnits
	 * @param processors
	 * @param options
	 * @return
	 * @throws IOException
	 */
	public static boolean compile(
			JavaCompiler compiler,
			Iterable<? extends JavaFileObject> compilationUnits,
			Iterable<? extends Processor> processors,
			Iterable<String> options)
			throws IOException
	{
		boolean result = false;

		StandardJavaFileManager standardJavaFileManager =
		     compiler.getStandardFileManager(null, Locale.getDefault(), null);

		DiagnosticCollector<JavaFileObject> diagnosticCollector =
		    new DiagnosticCollector<JavaFileObject>();

		CompilationTask compilationTask =
		    compiler.getTask(
		    		null, standardJavaFileManager, diagnosticCollector,
		        options, null, compilationUnits);

		compilationTask.setProcessors(processors);

		result = compilationTask.call();

		if (false == result)
		{
			String msg = diagnosticCollectorAsString(diagnosticCollector);
			LOGGER.warn(msg);
//			throw new IOException(msg);
		}
		
		return result;
	}

	/**
	 * See {@link #compile(JavaCompiler, Iterable, Iterable, Iterable)}.
	 *
	 * @param compilationUnits
	 * @param processors
	 * @param options
	 * @return value of {@link CompilationTask#call()}
	 * @throws IOException
	 */
	public static boolean compile(
			Iterable<? extends JavaFileObject> compilationUnits,
			Iterable<? extends Processor> processors,
			Iterable<String> options)
			throws IOException
	{
		return compile(createJavaCompiler(), compilationUnits, processors, options);
	}

	public static boolean runProcessorsOnly(
			Iterable<? extends JavaFileObject> compilationUnits,
			Iterable<? extends Processor> processors)
			throws IOException
	{
		return compile(compilationUnits, processors, asList("-proc:only"));
	}

	public static boolean runProcessorsOnly(
			JavaCompiler compiler,
			Iterable<? extends JavaFileObject> compilationUnits,
			Iterable<? extends Processor> processors)
			throws IOException
	{
		return compile(compiler, compilationUnits, processors, asList("-proc:only"));
	}

	public static boolean runProcessorsOnly(
			JavaCompiler compiler,
			Iterable<String> options,
			Iterable<? extends JavaFileObject> compilationUnits,
			Iterable<? extends Processor> processors)
			throws IOException
	{
		// add "-proc:only" to options if not already contained
		// - build a list from the iterable
		List<String> optionsAsList = new ArrayList<>();
		CollectionUtils.addAll(optionsAsList, options.iterator());
		// - try to find "-proc:only"
		if (false == optionsAsList.contains("-proc:only"))
		{
			optionsAsList.add("-proc:only");
		}

		return compile(compiler, compilationUnits, processors, optionsAsList);
	}

	public static String diagnosticCollectorAsString(
			DiagnosticCollector<JavaFileObject> diagnostics)
	{
		StringBuffer result = sb(lineSeparator() + "compilation failed");

		for (Diagnostic<? extends JavaFileObject> diagnostic :
		     diagnostics.getDiagnostics())
		{
			result.append(diagnosticAsString(diagnostic));
		}

		return result.toString();
	}
	
	private static String diagnosticAsString(
			Diagnostic<? extends JavaFileObject> diagnostic)
	{
		StringBuffer result = sb(lineSeparator());

		String source;

		if (diagnostic.getSource() == null)
		{
			source = "unknown source";
		}
		else
		{
			source = diagnostic.getSource().getName();
		}

		result.append(
				diagnostic.getCode() + ": " + source +
						"(" + diagnostic.getLineNumber() + ", " +
						      diagnostic.getColumnNumber() +
						"): " + diagnostic.getMessage(Locale.ENGLISH));

		return result.toString();
	}
}